﻿using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;

namespace Nature.Data
{
    /// <summary>
    /// Collection of linked Thermo7 objects representing a single Cp/H/S approximation
    /// </summary>
    [DebuggerDisplay("Count: {Count}, Tmin: {Tmin}, Tmax: {Tmax}")]
    [Serializable]
    public struct Thermo7Approximation : IEnumerable<Thermo7Range>, IEquatable<Thermo7Approximation>, ICloneable, IFormattable
    {
        #region Private Fields
        [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
        private Thermo7Range[] m_union;
        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the Thermo7Union class.
        /// </summary>
        /// <param name="union">Collection of Thermo7 objects to be linked as a single approximation</param>
        public Thermo7Approximation(IEnumerable<Thermo7Range> union)
            : this()
        {
            m_union = union.OrderBy(i => i.Tmin).ToArray();
            OnCreated();
        }

        /// <summary>
        /// Initializes a new instance of the Thermo7Union class.
        /// </summary>
        /// <param name="union">Collection of Thermo7 objects to be linked as a single approximation</param>
        /// <param name="ref">T/Cp/H/S reference to be assigned to the resulting approximation</param>
        public Thermo7Approximation(IEnumerable<Thermo7Range> union, ThermoRef @ref)
            : this()
        {
            m_union = union.OrderBy(i => i.Tmin).ToArray();
            var refRegion = m_union.Single(i => i.Tmin >= @ref.T && @ref.T <= i.Tmax);
            refRegion.Ref = @ref;
            int refIndex = Array.IndexOf(m_union, refRegion);
            for (int i = refIndex; i > 0; i--)
            {
                m_union[i - 1].Ref = m_union[i].RefLow;
            }
            for (int i = refIndex; i < m_union.Length - 1; ++i)
            {
                m_union[i + 1].Ref = m_union[i].RefHigh;
            }
            OnCreated();
        }

        private void OnCreated()
        {
            foreach (var item in m_union)
            {
                if (item.Tmin > item.Tmax) { throw new ArgumentException("item.Tmin > item.Tmax"); }
                if (item.Tmin < 0.0) { throw new ArgumentException("item.Tmin < 0"); }
            }
            DoubleTolerance tolerance = new DoubleTolerance(1.0e-8, 1.0e-8);
            for (int i = 1; i < m_union.Length; ++i)
            {
                if (false == m_union[i - 1].RefHigh.IsSame(m_union[i].RefLow, tolerance))
                {
                    throw new ArgumentException("m_union[i - 1].RefHigh != m_union[i].RefLow");
                }
            }
        }

        #region From Cp

        /// <summary>
        /// Initializes a new instance of the Thermo7Union class.
        /// </summary>
        /// <param name="ref">T/Cp/H/S reference to be assigned to the resulting approximation</param>
        /// <param name="temperatureValues">Discrete set of temperature points [K]</param>
        /// <param name="cpValuesLookup">Reduced heat capacity (Cp [~]) values vs temperatures [K]</param>
        /// <param name="maxAbsDev">Maximum absolute deviation from the given values</param>
        /// <param name="maxAbsDev">Maximum relative deviation from the given values</param>
        /// <returns>New instance of the Thermo7Union class approximating the given data</returns>
        public static Thermo7Approximation FromCpValues(
            ThermoRef @ref,
            IEnumerable<double> temperatureValues,
            Func<double, double> cpValuesLookup,
            double? maxAbsDev = null,
            double? maxRelDev = null)
        {
            CpThermo7Builder builder = new CpThermo7Builder();
            builder.Build(temperatureValues, cpValuesLookup);
            if (maxAbsDev.HasValue && builder.AbsoluteDeviation > maxAbsDev)
            {
                throw new Exception();
            }
            if (maxRelDev.HasValue && builder.RelativeDeviation > maxRelDev.Value)
            {
                throw new Exception();
            }
            return new Thermo7Approximation(builder.Ranges, @ref); ;
        }

        /// <summary>
        /// Initializes a new instance of the Thermo7Union class.
        /// </summary>
        /// <param name="ref">T/Cp/H/S reference to be assigned to the resulting approximation</param>
        /// <param name="temperatureValues">Discrete set of temperature points [K]</param>
        /// <param name="molarCpValuesLookup">Molar heat capacity (Cp) values vs temperatures [K]</param>
        /// <param name="maxAbsDev">Maximum absolute deviation from the given values</param>
        /// <param name="maxAbsDev">Maximum relative deviation from the given values</param>
        /// <returns>New instance of the Thermo7Union class approximating the given data</returns>
        public static Thermo7Approximation FromCpValues(
            ThermoRef @ref,
            IEnumerable<double> temperatureValues,
            Func<double, MolarHeatCapacity> molarCpValuesLookup,
            double? maxAbsDev = null,
            double? maxRelDev = null)
        {
            Func<double, double> reducedValuesLookup = (t) => molarCpValuesLookup(t) / Constants.Rgas;
            return Thermo7Approximation.FromCpValues(@ref, temperatureValues, reducedValuesLookup, maxAbsDev: maxAbsDev, maxRelDev: maxRelDev);
        }

        /// <summary>
        /// Initializes a new instance of the Thermo7Union class.
        /// </summary>
        /// <param name="ref">T/Cp/H/S reference to be assigned to the resulting approximation</param>
        /// <param name="temperatureValues">Discrete set of temperature points [K]</param>
        /// <param name="specificCpValuesLookup">Specific heat capacity (Cp) values vs temperatures [K]</param>
        /// <param name="componentMolarMass">Molar mass of the thermodynamic component</param>
        /// <param name="maxAbsDev">Maximum absolute deviation from the given values</param>
        /// <param name="maxAbsDev">Maximum relative deviation from the given values</param>
        /// <returns>New instance of the Thermo7Union class approximating the given data</returns>
        public static Thermo7Approximation FromCpValues(
            ThermoRef @ref,
            IEnumerable<double> temperatureValues,
            Func<double, SpecificHeatCapacity> specificCpValuesLookup,
            MolarMass componentMolarMass,
            double? maxAbsDev = null,
            double? maxRelDev = null)
        {
            Func<double, double> reducedValuesLookup = (t) => specificCpValuesLookup(t) * componentMolarMass / Constants.Rgas;
            return Thermo7Approximation.FromCpValues(@ref, temperatureValues, reducedValuesLookup, maxAbsDev: maxAbsDev, maxRelDev: maxRelDev);
        }

        #endregion

        #region From H

        /// <summary>
        /// Initializes a new instance of the Thermo7Union class.
        /// </summary>
        /// <param name="ref">T/Cp/H/S reference to be assigned to the resulting approximation</param>
        /// <param name="temperatureValues">Discrete set of temperature points [K]</param>
        /// <param name="reducedHValuesLookup">Reduced enthalpy (H [~]) values vs temperatures [K]</param>
        /// <param name="maxAbsDev">Maximum absolute deviation from the given values</param>
        /// <param name="maxAbsDev">Maximum relative deviation from the given values</param>
        /// <returns>New instance of the Thermo7Union class approximating the given data</returns>
        public static Thermo7Approximation FromHValues(
            ThermoRef @ref,
            IEnumerable<double> temperatureValues,
            Func<double, double> reducedHValuesLookup,
            double? maxAbsDev = null,
            double? maxRelDev = null)
        {
            HThermo7Builder builder = new HThermo7Builder();
            builder.Build(temperatureValues, reducedHValuesLookup);
            if (maxAbsDev.HasValue && builder.AbsoluteDeviation > maxAbsDev)
            {
                throw new Exception();
            }
            if (maxRelDev.HasValue && builder.RelativeDeviation > maxRelDev.Value)
            {
                throw new Exception();
            }
            return new Thermo7Approximation(builder.Ranges, @ref);
        }

        /// <summary>
        /// Initializes a new instance of the Thermo7Union class.
        /// </summary>
        /// <param name="ref">T/Cp/H/S reference to be assigned to the resulting approximation</param>
        /// <param name="temperatureValues">Discrete set of temperature points [K]</param>
        /// <param name="molarHValuesLookup">Molar enthalpy (H) values vs temperatures [K]</param>
        /// <param name="maxAbsDev">Maximum absolute deviation from the given values</param>
        /// <param name="maxAbsDev">Maximum relative deviation from the given values</param>
        /// <returns>New instance of the Thermo7Union class approximating the given data</returns>
        public static Thermo7Approximation FromHValues(
            ThermoRef @ref,
            IEnumerable<double> temperatureValues,
            Func<double, MolarEnthalpy> molarHValuesLookup,
            double? maxAbsDev = null,
            double? maxRelDev = null)
        {
            Func<double, double> reducedValuesLookup = (t) => molarHValuesLookup(t) / Constants.Rgas / t;
            return Thermo7Approximation.FromHValues(@ref, temperatureValues, reducedValuesLookup, maxAbsDev: maxAbsDev, maxRelDev: maxRelDev);
        }

        /// <summary>
        /// Initializes a new instance of the Thermo7Union class.
        /// </summary>
        /// <param name="ref">T/Cp/H/S reference to be assigned to the resulting approximation</param>
        /// <param name="temperatureValues">Discrete set of temperature points [K]</param>
        /// <param name="specificHValuesLookup">Specific enthalpy (H) values vs temperatures [K]</param>
        /// <param name="componentMolarMass">Molar mass of the thermodynamic component</param>
        /// <param name="maxAbsDev">Maximum absolute deviation from the given values</param>
        /// <param name="maxAbsDev">Maximum relative deviation from the given values</param>
        /// <returns>New instance of the Thermo7Union class approximating the given data</returns>
        public static Thermo7Approximation FromHValues(
            ThermoRef @ref,
            IEnumerable<double> temperatureValues,
            Func<double, SpecificEnthalpy> specificHValuesLookup,
            MolarMass componentMolarMass,
            double? maxAbsDev = null,
            double? maxRelDev = null)
        {
            Func<double, double> reducedValuesLookup = (t) => specificHValuesLookup(t) / Constants.Rgas / t * componentMolarMass;
            return Thermo7Approximation.FromHValues(@ref, temperatureValues, reducedValuesLookup, maxAbsDev: maxAbsDev, maxRelDev: maxRelDev);
        }
        #endregion

        #region From S
        /// <summary>
        /// Initializes a new instance of the Thermo7Union class.
        /// </summary>
        /// <param name="ref">T/Cp/H/S reference to be assigned to the resulting approximation</param>
        /// <param name="temperatureValues">Discrete set of temperature points [K]</param>
        /// <param name="reducedSValuesLookup">Reduced entropy (S [~]) values vs temperatures [K]</param>
        /// <param name="maxAbsDev">Maximum absolute deviation from the given values</param>
        /// <param name="maxAbsDev">Maximum relative deviation from the given values</param>
        /// <returns>New instance of the Thermo7Union class approximating the given data</returns>
        public static Thermo7Approximation FromSValues(
            ThermoRef @ref,
            IEnumerable<double> temperatureValues,
            Func<double, double> reducedSValuesLookup,
            double? maxAbsDev = null,
            double? maxRelDev = null)
        {
            SThermo7Builder builder = new SThermo7Builder();
            builder.Build(temperatureValues, reducedSValuesLookup);
            if (maxAbsDev.HasValue && builder.AbsoluteDeviation > maxAbsDev)
            {
                throw new Exception();
            }
            if (maxRelDev.HasValue && builder.RelativeDeviation > maxRelDev.Value)
            {
                throw new Exception();
            }
            return new Thermo7Approximation(builder.Ranges, @ref);
        }

        /// <summary>
        /// Initializes a new instance of the Thermo7Union class.
        /// </summary>
        /// <param name="ref">T/Cp/H/S reference to be assigned to the resulting approximation</param>
        /// <param name="temperatureValues">Discrete set of temperature points [K]</param>
        /// <param name="molarSValuesLookup">Molar entropy (S) values vs temperatures [K]</param>
        /// <param name="maxAbsDev">Maximum absolute deviation from the given values</param>
        /// <param name="maxAbsDev">Maximum relative deviation from the given values</param>
        /// <returns>New instance of the Thermo7Union class approximating the given data</returns>
        public static Thermo7Approximation FromSValues(
            ThermoRef @ref,
            IEnumerable<double> temperatureValues,
            Func<double, MolarEntropy> molarSValuesLookup,
            double? maxAbsDev = null,
            double? maxRelDev = null)
        {
            Func<double, double> reducedValuesLookup = (t) => molarSValuesLookup(t) / Constants.Rgas;
            return Thermo7Approximation.FromSValues(@ref, temperatureValues, reducedValuesLookup, maxAbsDev: maxAbsDev, maxRelDev: maxRelDev);
        }

        /// <summary>
        /// Initializes a new instance of the Thermo7Union class.
        /// </summary>
        /// <param name="ref">T/Cp/H/S reference to be assigned to the resulting approximation</param>
        /// <param name="temperatureValues">Discrete set of temperature points [K]</param>
        /// <param name="specificSValuesLookup">Specific entropy (S) values vs temperatures [K]</param>
        /// <param name="componentMolarMass">Molar mass of the thermodynamic component</param>
        /// <param name="maxAbsDev">Maximum absolute deviation from the given values</param>
        /// <param name="maxAbsDev">Maximum relative deviation from the given values</param>
        /// <returns>New instance of the Thermo7Union class approximating the given data</returns>
        public static Thermo7Approximation FromSValues(
            ThermoRef @ref,
            IEnumerable<double> temperatureValues,
            Func<double, SpecificEntropy> specificSValuesLookup,
            MolarMass componentMolarMass,
            double? maxAbsDev = null,
            double? maxRelDev = null)
        {
            Func<double, double> reducedValuesLookup = (t) => specificSValuesLookup(t) / Constants.Rgas * componentMolarMass;
            return Thermo7Approximation.FromSValues(@ref, temperatureValues, reducedValuesLookup, maxAbsDev: maxAbsDev, maxRelDev: maxRelDev);
        }
        #endregion

        #endregion

        #region Object Methods

        /// <summary>
        /// Indicates whether this instance and a specified object are equal.
        /// </summary>
        /// <param name="obj">Another object to compare to.</param>
        /// <returns>true if obj and this instance are the same type and represent the same value; otherwise, false.</returns>
        public override bool Equals(object obj)
        {
            if (obj is Thermo7Approximation)
            {
                return this.Equals((Thermo7Approximation)obj);
            }
            return base.Equals(obj);
        }

        /// <summary>
        /// Returns the hash code for this instance.
        /// </summary>
        /// <returns>A 32-bit signed integer that is the hash code for this instance.</returns>
        public override int GetHashCode()
        {
            int count = m_union.Length;
            return m_union.Select(i => i.GetHashCode() / count).Sum();
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets a 32-bit integer that represents the total number of Thermo7 objects in this instance
        /// </summary>
        public int Count { get { return m_union.Length; } }

        /// <summary>
        /// The lowest temperatures range Thermo7 object
        /// </summary>
        private Thermo7Range First { get { return m_union[0]; } }

        /// <summary>
        /// The highest temperatures range Thermo7 object
        /// </summary>
        private Thermo7Range Last { get { return m_union[m_union.Length - 1]; } }

        /// <summary>
        /// The lowest temperature [K]
        /// </summary>
        public double Tmin { get { return First.Tmin; } }

        /// <summary>
        /// The highest temperature [K]
        /// </summary>
        public double Tmax { get { return Last.Tmax; } }

        /// <summary>
        /// Reduced heat capacity (Cp [~]) at the lowest temperature
        /// </summary>
        public double Cpmin { get { return First.Cpmin; } }

        /// <summary>
        /// Reduced heat capacity (Cp [~]) at the highest temperature
        /// </summary>
        public double Cpmax { get { return Last.Cpmax; } }

        /// <summary>
        /// Reduced enthalpy (H [~]) at the lowest temperature
        /// </summary>
        public double Hmin { get { return First.Hmin; } }

        /// <summary>
        /// Reduced enthalpy (H [~]) at the highest temperature
        /// </summary>
        public double Hmax { get { return Last.Hmax; } }

        /// <summary>
        /// Reduced entropy (S [~]) at the lowest temperature
        /// </summary>
        public double Smin { get { return First.Smin; } }

        /// <summary>
        /// Reduced entropy (S [~]) at the highest temperature
        /// </summary>
        public double Smax { get { return Last.Smax; } }

        /// <summary>
        /// Reduced heat capacity (Cv [~]) at the lowest temperature
        /// </summary>
        public double Cvmin { get { return Cpmin - 1.0; } }

        /// <summary>
        /// Reduced heat capacity (Cv [~]) at the highest temperature
        /// </summary>
        public double Cvmax { get { return Cpmax - 1.0; } }

        /// <summary>
        /// Reduced internal energy (U [~]) at the lowest temperature
        /// </summary>
        public double Umin { get { return Hmin - 1.0; } }

        /// <summary>
        /// Reduced internal energy (U [~]) at the highest temperature
        /// </summary>
        public double Umax { get { return Hmax - 1.0; } }

        /// <summary>
        /// Reduced Helmholtz energy (F [~]) at the lowest temperature
        /// </summary>
        public double Fmin { get { return (Umin - Smin); } }

        /// <summary>
        /// Reduced Helmholtz energy (F [~]) at the highest temperature
        /// </summary>
        public double Fmax { get { return (Umax - Smax); } }

        /// <summary>
        /// Reduced Gibbs energy (G [~]) at the lowest temperature
        /// </summary>
        public double Gmin { get { return (Hmin - Smin); } }

        /// <summary>
        /// Reduced Gibbs energy (G [~]) at the highest temperature
        /// </summary>
        public double Gmax { get { return (Hmax - Smax); } }

        /// <summary>
        /// Gets the T/Cp/H/S reference point of this instance at the lowest temperature
        /// </summary>
        public ThermoRef RefLow { get { return First.RefLow; } }

        /// <summary>
        /// Gets the T/Cp/H/S reference point of this instance at the highest temperature
        /// </summary>
        public ThermoRef RefHigh { get { return Last.RefHigh; } }

        #endregion

        #region Public Methods

        /// <summary>
        /// Creates a new Thermo7Union object that is a deep copy of the current instance.
        /// </summary>
        /// <returns>A new Thermo7Union object that is a deep copy of this instance.</returns>
        public Thermo7Approximation Clone()
        {
            var data = m_union.ToArray();
            Thermo7Approximation union = new Thermo7Approximation();
            union.m_union = data;
            return union;
        }

   
        /// <summary>
        /// Extracts the T/Cp/H/S reference point from this intance for the given temperature value [K]
        /// </summary>
        /// <param name="t">temperature value [K]</param>
        /// <returns>Extracted T/Cp/H/S reference point</returns>
        public ThermoRef ToThermoRef(double t)
        {
            foreach (var item in m_union)
            {
                if (item.CoversTemperature(t))
                {
                    return new ThermoRef(t,
                        item.Cp(t) * Constants.Rgas,
                        item.H(t) * Constants.Rgas * t,
                        item.S(t) * Constants.Rgas);
                }
            }
            throw new ArgumentOutOfRangeException();
        }

        #endregion

        #region IEquatable<Thermo7Union> Members

        /// <summary>
        /// Indicates whether the current Thermo7Union instance is equal to another object of the same type.
        /// </summary>
        /// <param name="other">An object to compare with this object.</param>
        /// <returns>true if the current object is equal to the other parameter; otherwise, false.</returns>
        public bool Equals(Thermo7Approximation other)
        {
            if (Object.ReferenceEquals(other, null)) { return false; }
            if (Count != other.Count) { return false; }
            for (int i = 0; i < m_union.Length; ++i)
            {
                if (false == m_union[i].Equals(other.m_union[i])) { return false; }
            }
            return true;
        }

        #endregion

        #region IEnumerable<Thermo7> Members

        /// <summary>
        /// Returns an enumerator that iterates through the collection.
        /// </summary>
        /// <returns>A System.Collections.Generic.IEnumerator(Thermo7) that can be used to iterate through the collection.</returns>
        IEnumerator<Thermo7Range> IEnumerable<Thermo7Range>.GetEnumerator()
        {
            foreach (var thermo7 in m_union) { yield return thermo7; }
        }

        #endregion

        #region IEnumerable Members

        /// <summary>
        /// Returns an enumerator that iterates through a collection.
        /// </summary>
        /// <returns>An System.Collections.IEnumerator object that can be used to iterate through the collection.</returns>
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            foreach (var thermo7 in m_union) { yield return thermo7; }
        }

        #endregion

        #region ICloneable Members

        /// <summary>
        /// Creates a new object that is a copy of the current instance.
        /// </summary>
        /// <returns>A new object that is a copy of this instance.</returns>
        object ICloneable.Clone()
        {
            return this.Clone();
        }

        #endregion

        #region IFormattable Members

        public string ToString(string format, IFormatProvider formatProvider = null)
        {
            if (String.IsNullOrEmpty(format)) { format = "G"; }
            if (formatProvider == null) { formatProvider = CultureInfo.GetCultureInfo(""); }


            switch (format.ToUpperInvariant())
            {
                case "G":
                    {
                        StringBuilder sb = new StringBuilder();
                        for (int i = 0; i < m_union.Length; ++i)
                        {
                            Thermo7Range th7 = m_union[i];
                            sb.Append(i == 0 ? "" : "\r\n");
                            sb.AppendFormat("Tmax: {0} Tmin: {1}", th7.Tmin, th7.Tmax);
                            sb.AppendFormat(" A1: {0} A2: {1} A3: {2} A4: {3} A5: {4} A6: {5} A7: {6}",
                                th7.A1, th7.A2, th7.A3, th7.A4, th7.A5, th7.A6, th7.A7);
                        }
                        return sb.ToString();
                    }
                case "CK":
                    {
                        StringBuilder sb = new StringBuilder();
                        int endl = 1;
                        int elementsInRow = 0;
                        foreach (var th7 in m_union.OrderByDescending(i => i.Tmax))
                        {
                            foreach (var a in th7.ToArray())
                            {
                                sb.AppendFormat("{0, 15:0.00000000E+00}", a);
                                if (++elementsInRow >= 5) { sb.AppendFormat("{0,4}{1}\r\n", string.Empty, ++endl); elementsInRow = 0; }
                            }
                        }
                        while (elementsInRow++ < 5) { sb.AppendFormat("{0,15}", string.Empty); }
                        sb.AppendFormat("{0,4}{1}\r\n", string.Empty, ++endl);
                        return sb.ToString();
                    }
                case "CK1":
                    {
                        StringBuilder sb = new StringBuilder();
                        int counter = 0;
                        foreach (var th7 in m_union.OrderByDescending(i => i.Tmax))
                        {
                            foreach (var a in th7.ToArray())
                            {
                                sb.AppendFormat("{0, 15:0.00000000E+00}", a);
                                counter++;
                                if (counter == 5) { sb.AppendLine(); }
                                else if (counter == 7) { sb.AppendLine(); counter = 0; }
                            }
                        }
                        return sb.ToString();
                    }
                default:
                    throw new FormatException(String.Format("The {0} format string is not supported.", format));
            }
        }

        #endregion
    }
}
